嵌入式Linux小项目之图片编解码播放器(5)

您所在的位置:网站首页 小尾巴玩具 嵌入式图片 嵌入式Linux小项目之图片编解码播放器(5)

嵌入式Linux小项目之图片编解码播放器(5)

2024-07-15 02:56| 来源: 网络整理| 查看: 265

目录 一、jpg图片的显示原理分析1、认识jpg图片2、jpg图片如何显示3、如何解码jpg图片 二、libjpeg介绍及开源库的使用方法1、libjpeg介绍2、libjpeg版本及下载资源3、开源库的使用方法 三、libjpeg的移植实战1、移植2、部署 四、使用libjpeg解码显示jpg图片1、如何使用一个新的库2、libjpeg说明文档和示例代码3、结合说明文档和示例代码来一边学习一边编码一边实践5、编译调试6、部署动态库以使程序运行起来7、测试读取头信息8、解决解码显示中的问题9、结束jpg图片部分 附录——完整正确代码fb_jpeg.c

一、jpg图片的显示原理分析 1、认识jpg图片

  JPEG(Joint Photographic Experts Group)是JPEG标准的产物,该标准由国际标准化组织(ISO)制订,是面向连续色调静止图像的一种压缩标准。 JPEG格式是最常用的图像文件格式,后缀名为.jpg或.jpeg。

JPEG格式可分为标准JPEG、渐进式JPEG及JPEG2000三种格式: 1、 标准JPEG格式;此类型在网页下载时只能由上而下依序显示图像,直到图像资料全部下载完 毕,才能看到图像全貌。 2、渐进式JPEG;此类型在网页下载时,先呈现出图像的粗略外观后,再慢慢地呈现出完整的内 容,而且存成渐进式JPG格式的文档比存成标准JPG格式的文档要来得小,所以如果要在网页上使 用图像,可以多用这种格式。 3、JPEG2000;它是新一代的影像压缩法,压缩品质更高,并可改善在无线传输时,常因信号不 稳造成马赛克现象及位置错乱的情况,改善传输的品质。 压缩的比例越大,则损失越大相较于原图片。

(1)二进制文件

(2)有其固定的识别特征 http://www.cnblogs.com/Wendy_Yu/archive/2011/12/27/2303118.html 在这里插入图片描述 (3)经过压缩的图片格式   用空间换取时间,bmp格式的图片是可以直接拿去显示的,但jpg格式的图片涉及到了压缩与解压缩的过程。

2、jpg图片如何显示

(1)jpg图片中的二进制数并不对应像素数据,其是经过算法进行压缩后的数据 (2)LCD显示器的接口仍然是framebuffer (3)要显示jpg图片必须先解码jpg得到相应的位图数据

3、如何解码jpg图片

(1)图片编码和解码对应着压缩和解压缩过程

(2)编码和解码其实就是一些数学运算(压缩度、算法复杂度、时间、清晰度)

(3)软件编解码和硬件编解码 参考学习:https://blog.csdn.net/jakezhang1990/article/details/108335089      https://seoxiaoxin.com/13007.html 从性能角度而言硬件编解码要优于软件编解码,但一种格式的硬件编解码就需要一种相应的硬件模块,成本过高。

  软编码软解码主要依赖的是CPU资源,设备普通使用也是使用CPU做计算,所以开始编解码视频的时候CPU会飙升起来,发热就无法避免。

  硬编解码主要依赖的是GPU,这样就大大解放了CPU,性能上得到大大提升,在移动设备上主要使用的就是硬解码。在高分辨率视频流中也是使用硬件编码,现在主流是使用英伟达显卡。

(4)不同的图片格式其实就是编解码的算法不同,结果是图片特征不同

(5)编程实战:使用开源编解码库(软件编解码)

二、libjpeg介绍及开源库的使用方法 1、libjpeg介绍

(1)基于linux的开源软件(该软件的结构管理,开发方式与Linux类似) (2)C语言编写(gcc、Makefile管理) (3)提供JPEG图片的编解码算法实现

2、libjpeg版本及下载资源

(1)经典版本v6b:https://sourceforge.net/projects/libjpeg/files/libjpeg/6b/(本专栏文章使用这个) (2)最新版本v9b(当你看我文章的时候就不一定是最新了):http://www.ijg.org/

3、开源库的使用方法

(1)移植(源码下载、解压、配置、修改Makefile、编译或交叉编译)。   移植的目的是由源码得到三个东西:动态库.so,静态库.a,头文件.h

(2)部署(部署动态库so、部署静态库.a和头文件.h)

动态库是运行时环境需要的,编译程序时不需要。 静态库是静态连接时才需要,动态链接时不需要。 头文件.h是在编译程序时使用的,运行时不需要的。

  总结:静态库和头文件这两个东西,是在编译链接过程中需要的;而动态库是在运行时需要的。所以动态库so文件是要放到开发板的文件系统中去的(放的过程就叫部署),把静态库.a文件和头文件.h文件放到ubuntu的文件系统中去。

(3)注意三个编译链接选项:-I -l -L

-I是编译选项(准确的是说是预处理选项CFLAGS或者CPPFLAGS中指定),用来指定预处理时查找 头文件的范围的。 -l是链接选项(LDFLAGS中指定),用来指定链接额外的库(譬如我们用到了数学函数,就用 -lm,链接器就会去链接libm.so;那么我们使用了libjpeg,对应的库名字就叫libjpeg.so, 就需要用-ljpeg选项去链接) -L是链接选项(LDFLAGS中指定),用来告诉链接器到哪个路径下面去找动态链接库。

  总结:-l是告诉链接器要链接的动态库的名字,而-L是告诉链接器库的路径

三、libjpeg的移植实战 1、移植 (1)源码下载、解压(最好避免在共享文件中操作,否则可能会出现一些莫名奇妙的问题) tar -xvf jpegsrc.v6b.tar.gz mkdir /opt/libdecode/lib -p mkdir /opt/libdecode/include -p mkdir /opt/libdecode/bin -p (2)配置 ./configure --prefix=/opt/libdecode --exec-prefix=/opt/libdecode --enable-shared --enable-static -build=i386 -host=arm 报错:checking host system type... Invalid configuration `x86_64-unknown-linux-gnu 解决方法:移植libtool,获取两个配置文件config.guess、config.sub放到该源码包的源码目录下,再执行上述的配置命令 参考:https://blog.csdn.net/zd394071264/article/details/8286795 1、下载源码包:http://ftp.gnu.org/gnu/libtool/(选择与jpegsrc.v6b.tar.gz时间相近的版本) 2、在ubuntu 16.04中去移植,操作类似上述jpegsrc.v6b的步骤 (1)tar -zxvf libtool-2.2.2.tar.gz (2)cd libtool-2.2.2 (3)./configure --prefix=/home/decodeporting/jpeg-6b/libtool CC=arm-linux-gcc --host=arm (4)修改makefile: CXX = arm-linux-g++ CXXCPP = arm-linux-g++ -E (5)make (6)make install (7)make install生成的/home/decodeporting/jpeg-6b/libtool/share/libtool/config/目录下找到那两个配置文件放到/home/decodeporting/jpeg-6b/目录下即可 (3)Makefile检查,主要查看交叉编译设置是否正确 CC=gcc 改为 CC=arm-linux-gcc AR=ar rc 改为 AR=arm-linux-ar rc AR2=ranlib 改为  AR2=arm-linux-ranlib (4)编译 make (5)安装 make install-lib 安装就是将编译生成的库文件、头文件、可执行文件分别装载到--prefix --exec-prefix所指定的那些目录中去。

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

2、部署 部署动态链接库一般有三个位置可以考虑: 第一个:/lib 第二个:/usr/lib(本专栏使用根文件系统的该目录),cp *so* /home/rootfs/usr/lib/ -rf 第三个:任意指定目录

如果你使用静态库进行编译,则不需要部署静态链接库!

四、使用libjpeg解码显示jpg图片 1、如何使用一个新的库

(1)网络上找别人使用过后写的文档、博客等作为参考 (2)看库源码自带的文档(说明文档和示例代码)

2、libjpeg说明文档和示例代码

(1)说明文档:jpeg-6b文件夹中的README和libjpeg.doc libjpeg.doc (2)示例代码:example.c

3、结合说明文档和示例代码来一边学习一边编码一边实践

(1)解读example.c,read_JPEG_file函数是我们所需要的

struct jpeg_decompress_struct cinfo; /*该结构体包含JPEG解压缩参数和指向 *工作空间(由JPEG库根据需要分配)的指针。 * / struct my_error_mgr jerr; /*libjpeg库提供了一套错误处理机制,通过回调函数实现 *我们使用我们自己写的JPEG错误处理程序。 *,这个结构体必须和JPEG参数结构体一样长 * ,以避免悬空指针问题。 * / //对jpeg数据进行解码时,是一行一行进行的; FILE * infile; /* source file,指向原始jpeg文件 */ JSAMPARRAY buffer; /* Output row buffer,输出一行数据,jpeg解码时是一行一行进行的 */ int row_stride; /* physical row width in output buffer,一行的宽度 */ /*在这个例子:我们想要做任何其他事情之前都必须先打开文件, *可以在文件打开的情况下,使用setjmp()进行错误恢复(我们绑定的那个自己的错误处理函数) *非常重要的一点是:如果你在一台机器上打开二进制文件,fopen()要使用"b"选项 */ if ((infile = fopen(filename, "rb")) == NULL) { fprintf(stderr, "can't open %s\n", filename); return 0; }

通过以下八个步骤进行jpeg格式图片的解压缩

/* Step 1:申请分配内存初始化JPEG解压缩对象,上边定义的结构体都是指针*/ /*我们设置正常的JPEG错误例程,然后覆盖error_exit */ cinfo.err = jpeg_std_error(&jerr.pub); jerr.pub.error_exit = my_error_exit;//my_error_exit中绑定了我们自己的错误处理函数 //为我们使用的my_error_exit建立返回上下文 if (setjmp(jerr.setjmp_buffer)) {//若执行发生错误跳转到某个地方去运行 /*如果我们到达这里,JPEG代码已经发出了一个错误信号。 *我们需要清理JPEG对象,关闭打开的文件,然后返回。 * / jpeg_destroy_decompress(&cinfo); fclose(infile); return 0; } /*现在我们可以初始化JPEG解压缩所用的结构体。 */ jpeg_create_decompress(&cinfo); /* Step 2:指定数据源(如文件) */ jpeg_stdio_src(&cinfo, infile); /* Step 3: 使用jpeg_read_header()读取文件参数(头信息)*/ (void) jpeg_read_header(&cinfo, TRUE);// /* Step 4:设置解压缩参数*/ //在这里我们使用默认的参数去进行编解码,故而这里 //我们不需要进行任何设置 /* Step 5:开始解压缩*/ (void) jpeg_start_decompress(&cinfo); /* 输出缓冲区中每一行的样本数据 */ row_stride = cinfo.output_width * cinfo.output_components; /* w_stride = cinfo。output_width * cinfo.output_components; /*创建一个一行数据大小的样本数组,当图像处理完成时,该数组将消失 */ buffer = (*cinfo.mem->alloc_sarray) ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1); /* Step 6: while (scan lines remain to be read) */ /* jpeg_read_scanlines(...); 读一行去进行解码*/ /*这里我们使用标准库的状态变量cinfo。output_scanline(当前行数)作为 *循环计数器,这样我们就不必跟踪自己。cinfo.output_height最大行数 */ while (cinfo.output_scanline err really points to a my_error_mgr struct, so coerce pointer */ my_error_ptr myerr = (my_error_ptr) cinfo->err; /* Always display the message. */ /* We could postpone this until after returning, if we chose. */ (*cinfo->err->output_message) (cinfo); /* Return control to the setjmp point */ longjmp(myerr->setjmp_buffer, 1); } //参数列表:pPic:描述图片相关信息的结构体指针 //函数功能 :解析一张jpg格式的图片并进行显示 //返回值 :若失败则返回-1,若成功解码则返回0 int jpg_analyze(pic_info *pPic) { struct jpeg_decompress_struct cinfo;/* 贯穿整个解压缩过程的一个结构体 */ struct my_error_mgr jerr;/* 错误处理机制相关的结构体 */ FILE * infile; /* 指向原始jpeg文件 */ JSAMPARRAY buffer = NULL; /* 指向解码行数据的指针,一行一行进行解码 */ int row_stride; /* 解码出来一行图片信息的字节数 */ if ((infile = fopen(pPic->pathname, "rb")) == NULL) /* 打开并读取jpg格式的二进制文件 */ { fprintf(stderr, "can't open %s\n", pPic->pathname); return -1; } /* Step 1: 申请获取所需的资源并进行初始化 */ cinfo.err = jpeg_std_error(&jerr.pub);/* 错误处理函数的绑定 */ jerr.pub.error_exit = my_error_exit; if (setjmp(jerr.setjmp_buffer)) { jpeg_destroy_decompress(&cinfo); fclose(infile); return 0; } jpeg_create_decompress(&cinfo);/* 给解码器进行必要的内存分配和初始化 */ /* Step 2: 将打开的源jpg文件和解码器相关联 */ jpeg_stdio_src(&cinfo, infile); /* Step 3: 读取jpg图片的头信息 */ (void) jpeg_read_header(&cinfo, TRUE); /* Step 4: set parameters for decompression */ //在这里我们使用默认的参数去进行编解码,故而这里 //我们不需要进行任何设置 /* Step 5: 开始解码 */ (void) jpeg_start_decompress(&cinfo); DBG("image solution :%d*%d, bpp/8=%d.\n", cinfo.output_width, cinfo.output_height, cinfo.output_components); //解码出来的一行数据的字节数 row_stride = cinfo.output_width * cinfo.output_components; buffer = (*cinfo.mem->alloc_sarray) ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1); if (NULL == buffer) { fprintf(stderr, "cinfo.mem->alloc_sarray error.\n"); return -1; } /* Step 6: 逐行解码,并将解码出的数据丢到事先准备好的缓冲区中 */ while (cinfo.output_scanline pData + cinfo.output_scanline * row_stride), buffer, row_stride); } /* Step 7: 解码完成 */ (void) jpeg_finish_decompress(&cinfo); /* Step 8: 进行清理工作 */ jpeg_destroy_decompress(&cinfo); fclose(infile); //显示图片 pPic->width = cinfo.output_width; pPic->height = cinfo.output_height; pPic->bpp = cinfo.output_components * 8; lcd_display_picture(0, 0, pPic); return 0; } 5、编译调试

(1)测试代码,先试图读取jpg图片头信息 (2)问题排除: 编译时问题:主要就是头文件包含(那个头文件在前那个在后也是有一定影响的),除了在代码中包含头文件外,还要注意指明头文件的路径,注意-I、-l、-L三个编译链接选项的使用

修改Makefile: CFLAGS += -I $(shell pwd)/include -I/opt/libdecode/include #size_t数据类型包含在stdio.h头文件中 6、部署动态库以使程序运行起来

(1)第一种情况:放到/lib或者/usr/lib下,这样不需要给系统指定库路径,就能自动找到。(强调一下是开发板根文件系统下的路径,千万不要弄成了ubuntu的根文件系统下的目录)

(2)第二种情况:有时候对于一些不常用的库,我们不愿意将他放到lib或usr/lib目录下去,而是找个自定义的第三方的目录,单独将这些不常用的库放置,然后再用一定方法告诉操作系统去到这个路径下加载这个库。将该自定义第三方目录导出到环境变量LD_LIBRARY_PATH下即可。

export LD_LIBRARY_PATH=/opt/mylib:$LD_LIBRARY_PATH#导出环境变量 echo $LD_LIBRARY_PATH#查看是否导出 7、测试读取头信息 int jpg_analyze(pic_info *pPic) { struct jpeg_decompress_struct cinfo;/* 贯穿整个解压缩过程的一个结构体 */ struct my_error_mgr jerr;/* 错误处理机制相关的结构体 */ FILE * infile; /* 指向原始jpeg文件 */ JSAMPARRAY buffer; /* 指向解码行数据的指针,一行一行进行解码 */ int row_stride; /* 解码出来一行图片信息的字节数 */ if ((infile = fopen(pPic->pathname, "rb")) == NULL) /* 打开并读取jpg格式的二进制文件 */ { fprintf(stderr, "can't open %s\n", pPic->pathname); return -1; } /* Step 1: 申请获取所需的资源并进行初始化 */ cinfo.err = jpeg_std_error(&jerr.pub);/* 错误处理函数的绑定 */ jerr.pub.error_exit = my_error_exit; if (setjmp(jerr.setjmp_buffer)) { jpeg_destroy_decompress(&cinfo); fclose(infile); return 0; } jpeg_create_decompress(&cinfo);/* 给解码器进行必要的内存分配和初始化 */ /* Step 2: 将打开的源jpg文件和解码器相关联 */ jpeg_stdio_src(&cinfo, infile); /* Step 3: 读取jpg图片的头信息 */ (void) jpeg_read_header(&cinfo, TRUE); DBG("image solution :%d*%d, bpp/8=.\n", cinfo.output_width, cinfo.output_height, cinfo.output_components); return 0; }

在这里插入图片描述

/* Step 3: 读取jpg图片的头信息 */ (void) jpeg_read_header(&cinfo, TRUE); /* Step 4: set parameters for decompression */ /* Step 5: Start decompressor */ (void) jpeg_start_decompress(&cinfo); DBG("image solution :%d*%d, bpp/8=%d.\n", cinfo.output_width, cinfo.output_height, cinfo.output_components);

在这里插入图片描述

8、解决解码显示中的问题

原图: 在这里插入图片描述 实际显示效果: 在这里插入图片描述 黑色部分为我们要显示的jpg格式的图片,由图可知一片黑色,没有正常显示。

问题分析及解决记录: (1)根据LCD上错误的显示状态,分析有可能是显示函数中的图片宽高数据有误导致的,于是在图片显示函数fb_draw中添加debug打印出宽和高来,结果发现是对的。

(2)显示函数中的图片宽高和fb宽高都是对的,结果显示时还是异常,可能的一个原因就是:显示数据本身不对,黑色代表很多数据都是0。如何验证?只要把显示数据打印出来看一看就知道了。结果发现打印出的待显示数据果然是很多0,说明给显示函数的待显示数据就是错的

(3)这些待显示数据为什么会错?

第一种可能性就是libjpeg解码出来就是错的;

第二种可能性是解码出来再暂存的时候,或者从暂存的buffer拿出来的时候给搞错了。相对来说第二种很好验证而第一种不好验证。我们只需要在jpeg_read_scanlines函数后面直接打印显示解码出来的一行数据,就可以知道是不是第二种情况了。结果打印出来好多0,说明是第一种情况。

(4)截至目前,已经锁定了问题,就是jpeg_read_scanlines解码出来的数据本身就不对。

(5)可能的问题有:有可能是libjpeg本身就有问题;有可能我们对libjpeg的部署不对导致他工作不对;有可能我们写的代码不对,也就是说我们没用正确的方法来使用libjpeg。

(6)走投无路没有思路,不知道查谁,怎么办?

方法一:去网上找一些别人写的libjpeg解码显示图片的示例代码,多看几个,对着和我们的关键部位对比,寻找思路。

方法二:如果在网上找不到相关资料,这时候就只有硬着头皮去看源码了。譬如我们这里,就要去libjpeg的源码中查看:jpeg_read_scanlines、cinfo.mem->alloc_sarray等

(7)最终我们发现是我们写的代码不对,他这个提供的example.c属实有点坑. 解决了buffer申请导致的问题之后,我们又发现2个遗留的问题:一个就是RGB顺序问题(若显示正常则不用修改,方法之前的文章有讲过),另一个是图像转了180度的问题。 在这里插入图片描述 (8)添加了fb_draw2函数并且调用后,通过修改显示代码2个遗留问题彻底解决。至此,jpg图片显示完美实现。

9、结束jpg图片部分

(1)加上jpg图片格式识别

//参数列表:path:要解析的图片的文件名 //函数功能 :判断一个图片文件是否为一个合法的jpg文件 //返回值 :若不是则返回1,若是则返回0, 错误返回-1 int is_jpg(const char *path) { FILE *file = NULL; char buf[2]={0}; //打开文件 file = fopen(path, "rb"); if (NULL == file) { fprintf(stderr, "fopen %s error.\n", path); return -1; } //读取前两个字节 fread(buf, sizeof(char), sizeof(buf), file); //判断是不是0xffd8 if (!((buf[0] == 0xff) && (buf[1] == 0xd8))) { return 1;//不是jpg图片 } DBG("The first two bytes of the file are:%x%x.\n", buf[0], buf[1]); //若是0xffd8开头,则继续读取 fseek(file, -2, SEEK_END);//文件指针移动到倒数两个字符的位置 fread(buf, sizeof(char), sizeof(buf), file); //判断是不是0xffd9 if (!((buf[0] == 0xff) && (buf[1] == 0xd9))) { return 1;//不是jpg图片 } DBG("The last two bytes of the file are:%x%x.\n", buf[0], buf[1]); fclose(file);//关闭文件 DBG("This is a JPEG picture.\n"); return 0; }

(2)对外封装好用的jpg图片显示函数

void lcd_display_jpeg(int x0, int y0, pic_info *pPic) { const unsigned char *pData = pPic->pData;//指针指向图像数据数组 unsigned int x, y, color, p = 0; if ((pPic->bpp != 32) && (pPic->bpp != 24)) { fprintf(stderr, "BPP %d is not support.\n", pPic->bpp); } for(y = y0; y height+y0); y++) { if(y > HEIGHT) break; for(x = x0; x width+x0); x++) { if(x > WIDTH) { p += 3; continue; } color = ((pData[p+2] height*3-3 ; for(y = y0; y height+y0); y++) { if(y > HEIGHT) break; for(x = x0; x width+x0); x++) { if(x > WIDTH) { p += 3; continue; } color = ((pData[p+2] pathname, "rb")) == NULL) /* 打开并读取jpg格式的二进制文件 */ { fprintf(stderr, "can't open %s\n", pPic->pathname); return -1; } /* Step 1: 申请获取所需的资源并进行初始化 */ cinfo.err = jpeg_std_error(&jerr.pub);/* 错误处理函数的绑定 */ jerr.pub.error_exit = my_error_exit; if (setjmp(jerr.setjmp_buffer)) { jpeg_destroy_decompress(&cinfo); fclose(infile); return 0; } jpeg_create_decompress(&cinfo);/* 给解码器进行必要的内存分配和初始化 */ /* Step 2: 将打开的源jpg文件和解码器相关联 */ jpeg_stdio_src(&cinfo, infile); /* Step 3: 读取jpg图片的头信息 */ (void) jpeg_read_header(&cinfo, TRUE); /* Step 4: set parameters for decompression */ //在这里我们使用默认的参数去进行编解码,故而这里 //我们不需要进行任何设置 /* Step 5: 开始解码 */ (void) jpeg_start_decompress(&cinfo); DBG("image solution :%d*%d, bpp/8=%d.\n", cinfo.output_width, cinfo.output_height, cinfo.output_components); //解码出来的一行数据的字节数 row_stride = cinfo.output_width * cinfo.output_components; //buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1); buffer = (char *)malloc(row_stride); if (NULL == buffer) { fprintf(stderr, "cinfo.mem->alloc_sarray error.\n"); return -1; } /* Step 6: 逐行解码,并将解码出的数据丢到事先准备好的缓冲区中 */ while (cinfo.output_scanline pData + cinfo.output_scanline * row_stride), buffer, row_stride); } /* Step 7: 解码完成 */ (void) jpeg_finish_decompress(&cinfo); /* Step 8: 进行清理工作 */ jpeg_destroy_decompress(&cinfo); fclose(infile); //显示图片 pPic->width = cinfo.output_width; pPic->height = cinfo.output_height; pPic->bpp = cinfo.output_components * 8; lcd_display_jpeg(0, 0, pPic); return 0; } //封装一个对外使用的jpg显示函数,本函数对外只需要一个jpg图片的 //pathname即可,那些复杂的显示数据处理过程在显示模块内部处理, //正确显示图片返回0,错误返回-1 int display_jpg(const char*pathname) { int ret = -1; pic_info jpeg; //第一步:检测给的图片是不是jpg图片 ret = is_jpg(pathname); if (ret != 0) { return -1; } //第二步:显示该jpg图片 jpeg.pathname = pathname; jpeg.pData = rgb_buf; jpg_analyze(&jpeg); return 0; }

正确显示的结果: 在这里插入图片描述

注:本资料大部分由朱老师物联网大讲堂课程笔记整理而来并且引用了部分他人博客的内容,如有侵权,联系删除!水平有限,如有错误,欢迎各位在评论区交流。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3